﻿/*
 * This file is part of MonoSettlers.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://opensettlers.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using OpenTK;
using OpenTK.Input;
using OpenTK.Graphics;
using System.Runtime.InteropServices;
using System.Threading;

#if EMBEDDED
    using OpenTK.Graphics.ES20;
#else
using OpenTK.Graphics.OpenGL;
#endif

namespace MonoSettlers.RenderSystem
{
    public partial class GLRenderer : IRenderSystem
    {
        private GLRenderer(int inMapSize)
        {
            MapSize = inMapSize;
            Selection = new SelectionModel(this);

            m_ProjMatrix = m_ViewMatrix = m_ModelMatrix = Matrix4.Identity;
        }

        /// <summary>
        /// One time initialization of a new GL context. Must be called from within the render thread!
        /// </summary>
        /// <param name="inPixelWidth">Pixel width of the viewport.</param>
        /// <param name="inPixelHeight">Pixel height of the viewport.</param>
        private void InitializeGLContext(int inPixelWidth, int inPixelHeight)
        {
            // just dump some system information into log file which will be important for bugreports.
            String logEntry = "Creating OpenGL Renderer...\r\n" +
                "    => OpenGL Version: \"" + GL.GetString(StringName.Version) + "\"\r\n" +
                "    => GLSL Version: \"" + GL.GetString(StringName.ShadingLanguageVersion) + "\"\r\n" +
                "    => OpenGL Vendor: \"" + GL.GetString(StringName.Vendor) + "\"\r\n" +
                "    => OpenGL Renderer: \"" + GL.GetString(StringName.Renderer) + "\"\r\n" +
                "    => OpenGL Extensions: \"\r\n        " + GL.GetString(StringName.Extensions).Replace(" ", "\r\n        ") + "\"\r\n";

            Log.LogMessage(logEntry);

            // load textures for building grid
            m_BuildingGridTextures = new NativeTexture[6];

            for (int i = 0; i < m_BuildingGridTextures.Length; i++)
            {
                m_BuildingGridTextures[i] = new NativeTexture(this, (Bitmap)Bitmap.FromFile(@".\Resources\Icons\BuildingGrid\Level_0" + i + ".png"));
            }

            GL.ClearColor(Color.DarkGreen);
            GL.ClearDepth(1.0f);

            // load shaders
            m_2DSceneProgram = new GLProgram(new GLShader(@".\Resources\Shaders\2DScene.vert"), new GLShader(@".\Resources\Shaders\2DScene.frag"), true);
            m_2DSceneSelectProgram = new GLProgram(new GLShader(@".\Resources\Shaders\2DSceneSelect.vert"), new GLShader(@".\Resources\Shaders\2DSceneSelect.frag"), true);
        }

        /// <summary>
        /// Should be called whenever the surface changes its size and properly
        /// updates the GL viewport and other related properties.
        /// </summary>
        /// <param name="inPixelWidth">Pixel width of the viewport.</param>
        /// <param name="inPixelHeight">Pixel height of the viewport.</param>
        private void UpdateViewport(int inPixelWidth, int inPixelHeight)
        {
            if ((inPixelWidth <= 0) || (inPixelHeight <= 0))
                return;

            GL.Viewport(0, 0, inPixelWidth, inPixelHeight);

            Scale = VisualsPerScreenLine / 900;
            ViewportWidth = inPixelWidth;
            ViewportHeight = inPixelHeight;

            m_ProjMatrix = Matrix4.CreateOrthographicOffCenter(
                0,
                (float)VisualsPerScreenLine,
                (float)(VisualsPerScreenLine * ((Double)inPixelHeight / inPixelWidth)),
                0, -1, 1);
        }

        /// <summary>
        /// Used in AnimationEditor and temporarily to use WinForms in MonoSettlers as long as there
        /// is no pure OpenGL GUI available.
        /// </summary>
        public static GLRenderer CreateInControl(int inMapSize, System.Windows.Forms.ContainerControl inContainer)
        {
            GLRenderer result = new GLRenderer(inMapSize);
            GLControl glCtrl = new GLControl();
            bool isInitialized = false;

            result.OnMouseEnter += (unused, visual) =>
            {
                visual.IsVisible = false;
            };

            glCtrl.Dock = System.Windows.Forms.DockStyle.Fill;

            // also update viewport on resize
            glCtrl.SizeChanged += (sender, args) =>
            {
                result.UpdateViewport(glCtrl.Width, glCtrl.Height);
            };

            // is invoked whenever the control is to be repaint
            glCtrl.Paint += (sender, args) =>
            {
                try
                {
                    if (!isInitialized)
                    {
                        result.InitializeGLContext(glCtrl.Width, glCtrl.Height);
                        
                        isInitialized = true;
                    }

                    result.OnRender();

                    glCtrl.SwapBuffers();
                }
                catch (Exception e)
                {
                    Log.LogExceptionCritical(e);
                }
            };

            // intercept mouse movement
            glCtrl.MouseMove += (sender, args) =>
            {
                result.RaiseMouseMove(args.X, args.Y);
            };

            // the following represents the rendering loop
            if (result.m_CtrlTimer != null)
                result.m_CtrlTimer.Stop();

            result.m_CtrlTimer = new System.Windows.Forms.Timer();
            result.m_CtrlTimer.Interval = 30;
            result.m_CtrlTimer.Tick += (sender, args) =>
            {
                // stopping the timer prevents CPU freeze, if rendering takes longer than desired frame rate.
                try
                {
                    result.m_CtrlTimer.Stop();

                    glCtrl.Invalidate();
                }
                finally
                {
                    result.m_CtrlTimer.Start();
                }
            };
            result.m_CtrlTimer.Start();

            // finally attach the control to its parent
            inContainer.Controls.Add(glCtrl);

            return result;
        }

        /// <summary>
        /// Creates a new game window with its own render thread. Use <see cref="WaitForTermination"/>
        /// to join it.
        /// </summary>
        public static GLRenderer CreateAsWindow(int inMapSize, params String[] inArgs)
        {
            GLRenderer result = new GLRenderer(inMapSize);
            System.Threading.Thread thread = new System.Threading.Thread(() =>
            {
                try
                {
                    var m_Window = new GameWindow(800, 600, GraphicsMode.Default, "MonoSettlers MILESTONE 0", GameWindowFlags.Default);
                    m_Window.Visible = true;

                    result.InitializeGLContext(m_Window.Width, m_Window.Height);

                    m_Window.Mouse.ButtonDown += result.Mouse_ButtonDown;
                    m_Window.Mouse.ButtonUp += result.Mouse_ButtonUp;
                    m_Window.Keyboard.KeyDown += result.Keyboard_KeyDown;
                    m_Window.Keyboard.KeyUp += result.Keyboard_KeyUp;

                    while (true)
                    {
                        m_Window.ProcessEvents();

                        if (m_Window.IsExiting)
                            return;

                        result.UpdateKeyboardState();
                        result.UpdateMouseState();

                        result.OnRender();

                        m_Window.SwapBuffers();

                        System.Threading.Thread.Sleep(30);
                    }
                }
                finally
                {
                    result.IsTerminated = true;

                    result.Dispose();
                }
            });

            thread.Start();

            return result;
        }

        /// <summary>
        /// Only useful in conjunction with <see cref="CreateAsWindow"/> and waits until the main render
        /// thread exits.
        /// </summary>
        public void WaitForTermination()
        {
            while (!IsTerminated)
                System.Threading.Thread.Sleep(100);
        }

        /// <summary>
        /// In case of <see cref="CreateAsWindow"/> this method is called automatically on exit.
        /// In case of <see cref="CreateInControl"/> you have to call it yourself when the renderer
        /// is no longer needed.
        /// </summary>
        public void Dispose()
        {
            if (m_CtrlTimer != null)
            {
                m_CtrlTimer.Stop();
                m_CtrlTimer.Dispose();
                m_CtrlTimer = null;
            }

            // release textures
            foreach (var tex in m_Textures.ToArray())
            {
                tex.Dispose();
            }

            m_Textures.Clear();

            // release shaders
            if (m_2DSceneProgram != null)
                m_2DSceneProgram.Dispose();

            if (m_2DSceneSelectProgram != null)
                m_2DSceneSelectProgram.Dispose();

            m_2DSceneProgram = null;
            m_2DSceneSelectProgram = null;
        }
    }
}
